Passed
Push — master ( 6e4ba7...34794c )
by Rafael S.
02:21
created

index.js ➔ getLISTINFOIndex_   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 3
dl 0
loc 11
rs 10
nop 0
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from './vendor/bitdepth.js';
33
import * as imaadpcm from './vendor/imaadpcm.js';
34
import * as alawmulaw from './vendor/alawmulaw.js';
35
import {encode, decode} from './vendor/base64-arraybuffer-es6.js';
36
import {unpackArray, packArrayTo, unpackArrayTo} from './vendor/byte-data.js';
37
import makeWavHeader from './lib/make-wav-header.js';
38
import validateWavHeader from './lib/validate-wav-header';
39
import {riffChunks, findChunk_} from './vendor/riff-chunks.js';
40
import BufferIO from './lib/bufferio.js';
41
import writeWavBuffer from './lib/wav-buffer-writer.js';
42
import readWavBuffer from './lib/wav-buffer-reader.js';
43
import WavBuffer from './lib/wav-buffer.js';
44
45
/**
46
 * Class representing a wav file.
47
 * @extends WavBuffer
48
 * @ignore
49
 */
50
export default class WaveFile extends WavBuffer {
51
52
  /**
53
   * @param {?Uint8Array} bytes A wave file buffer.
54
   * @throws {Error} If no 'RIFF' chunk is found.
55
   * @throws {Error} If no 'fmt ' chunk is found.
56
   * @throws {Error} If no 'data' chunk is found.
57
   */
58
  constructor(bytes=null) {
59
    super();
60
    /**
61
     * The bit depth code according to the samples.
62
     * @type {string}
63
     */
64
    this.bitDepth = '0';
65
    /**
66
     * @type {!Object}
67
     * @private
68
     */
69
    this.dataType = {};
70
    this.io = new BufferIO();
71
    // Load a file from the buffer if one was passed
72
    // when creating the object
73
    if (bytes) {
74
      this.fromBuffer(bytes);
75
    }
76
  }
77
78
  /**
79
   * Set up the WaveFile object based on the arguments passed.
80
   * @param {number} numChannels The number of channels
81
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
82
   * @param {number} sampleRate The sample rate.
83
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
84
   * @param {string} bitDepthCode The audio bit depth code.
85
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
86
   *    or any value between '8' and '32' (like '12').
87
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
88
   *    The samples. Must be in the correct range according to the bit depth.
89
   * @param {?Object} options Optional. Used to force the container
90
   *    as RIFX with {'container': 'RIFX'}
91
   * @throws {Error} If any argument does not meet the criteria.
92
   */
93
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
94
    if (!options.container) {
95
      options.container = 'RIFF';
96
    }
97
    this.container = options.container;
98
    this.bitDepth = bitDepthCode;
99
    samples = this.interleave_(samples);
100
    this.updateDataType_();
101
    /** @type {number} */
102
    let numBytes = this.dataType.bits / 8;
103
    this.data.samples = new Uint8Array(samples.length * numBytes);
104
    packArrayTo(samples, this.dataType, this.data.samples);
105
    /** @type {!Object} */
106
    let header = makeWavHeader(
107
      bitDepthCode, numChannels, sampleRate,
108
      numBytes, this.data.samples.length, options);
109
    this.clearHeader_();
110
    this.chunkSize = header.chunkSize;
111
    this.format = header.format;
112
    this.fmt = header.fmt;
113
    if (header.fact) {
114
      this.fact = header.fact;
115
    }
116
    this.data.chunkId = 'data';
117
    this.data.chunkSize = this.data.samples.length;
118
    validateWavHeader(this);
119
  }
120
121
  /**
122
   * Set up the WaveFile object from a byte buffer.
123
   * @param {!Uint8Array} bytes The buffer.
124
   * @param {boolean=} samples True if the samples should be loaded.
125
   * @throws {Error} If container is not RIFF, RIFX or RF64.
126
   * @throws {Error} If no 'fmt ' chunk is found.
127
   * @throws {Error} If no 'data' chunk is found.
128
   */
129
  fromBuffer(bytes, samples=true) {
130
    this.clearHeader_();
131
    readWavBuffer(bytes, samples, this);
132
    this.bitDepthFromFmt_();
133
    this.updateDataType_();
134
  }
135
136
  /**
137
   * Return a byte buffer representig the WaveFile object as a .wav file.
138
   * The return value of this method can be written straight to disk.
139
   * @return {!Uint8Array} A .wav file.
140
   * @throws {Error} If any property of the object appears invalid.
141
   */
142
  toBuffer() {
143
    validateWavHeader(this);
144
    return writeWavBuffer(this);
145
  }
146
147
  /**
148
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
149
   * @param {string} base64String A .wav file as a base64 string.
150
   * @throws {Error} If any property of the object appears invalid.
151
   */
152
  fromBase64(base64String) {
153
    this.fromBuffer(new Uint8Array(decode(base64String)));
154
  }
155
156
  /**
157
   * Return a base64 string representig the WaveFile object as a .wav file.
158
   * @return {string} A .wav file as a base64 string.
159
   * @throws {Error} If any property of the object appears invalid.
160
   */
161
  toBase64() {
162
    /** @type {!Uint8Array} */
163
    let buffer = this.toBuffer();
164
    return encode(buffer, 0, buffer.length);
165
  }
166
167
  /**
168
   * Return a DataURI string representig the WaveFile object as a .wav file.
169
   * The return of this method can be used to load the audio in browsers.
170
   * @return {string} A .wav file as a DataURI.
171
   * @throws {Error} If any property of the object appears invalid.
172
   */
173
  toDataURI() {
174
    return 'data:audio/wav;base64,' + this.toBase64();
175
  }
176
177
  /**
178
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
179
   * @param {string} dataURI A .wav file as DataURI.
180
   * @throws {Error} If any property of the object appears invalid.
181
   */
182
  fromDataURI(dataURI) {
183
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
184
  }
185
186
  /**
187
   * Force a file as RIFF.
188
   */
189
  toRIFF() {
190
    this.fromScratch(
191
      this.fmt.numChannels,
192
      this.fmt.sampleRate,
193
      this.bitDepth,
194
      unpackArray(this.data.samples, this.dataType));
195
  }
196
197
  /**
198
   * Force a file as RIFX.
199
   */
200
  toRIFX() {
201
    this.fromScratch(
202
      this.fmt.numChannels,
203
      this.fmt.sampleRate,
204
      this.bitDepth,
205
      unpackArray(this.data.samples, this.dataType),
206
      {container: 'RIFX'});
207
  }
208
209
  /**
210
   * Change the bit depth of the samples.
211
   * @param {string} newBitDepth The new bit depth of the samples.
212
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
213
   * @param {boolean} changeResolution A boolean indicating if the
214
   *    resolution of samples should be actually changed or not.
215
   * @throws {Error} If the bit depth is not valid.
216
   */
217
  toBitDepth(newBitDepth, changeResolution=true) {
218
    /** @type {string} */
219
    let toBitDepth = newBitDepth;
220
    /** @type {string} */
221
    let thisBitDepth = this.bitDepth;
222
    if (!changeResolution) {
223
      if (newBitDepth != '32f') {
224
        toBitDepth = this.dataType.bits.toString();
225
      }
226
      thisBitDepth = this.dataType.bits;
227
    }
228
    this.assureUncompressed_();
229
    /** @type {number} */
230
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
231
    /** @type {!Float64Array} */
232
    let typedSamplesInput = new Float64Array(sampleCount + 1);
233
    /** @type {!Float64Array} */
234
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
235
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
236
    bitDepthLib(
237
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
238
    this.fromScratch(
239
      this.fmt.numChannels,
240
      this.fmt.sampleRate,
241
      newBitDepth,
242
      typedSamplesOutput,
243
      {container: this.correctContainer_()});
244
  }
245
246
  /**
247
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
248
   * @throws {Error} If sample rate is not 8000.
249
   * @throws {Error} If number of channels is not 1.
250
   */
251
  toIMAADPCM() {
252
    if (this.fmt.sampleRate !== 8000) {
253
      throw new Error(
254
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
255
    } else if (this.fmt.numChannels !== 1) {
256
      throw new Error(
257
        'Only mono files can be compressed as IMA-ADPCM.');
258
    } else {
259
      this.assure16Bit_();
260
      let output = new Int16Array(this.data.samples.length / 2);
261
      unpackArrayTo(this.data.samples, this.dataType, output);
262
      this.fromScratch(
263
        this.fmt.numChannels,
264
        this.fmt.sampleRate,
265
        '4',
266
        imaadpcm.encode(output),
267
        {container: this.correctContainer_()});
268
    }
269
  }
270
271
  /**
272
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
273
   * @param {string} bitDepthCode The new bit depth of the samples.
274
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
275
   *    Optional. Default is 16.
276
   */
277
  fromIMAADPCM(bitDepthCode='16') {
278
    this.fromScratch(
279
      this.fmt.numChannels,
280
      this.fmt.sampleRate,
281
      '16',
282
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
283
      {container: this.correctContainer_()});
284
    if (bitDepthCode != '16') {
285
      this.toBitDepth(bitDepthCode);
286
    }
287
  }
288
289
  /**
290
   * Encode a 16-bit wave file as 8-bit A-Law.
291
   */
292
  toALaw() {
293
    this.assure16Bit_();
294
    let output = new Int16Array(this.data.samples.length / 2);
295
    unpackArrayTo(this.data.samples, this.dataType, output);
296
    this.fromScratch(
297
      this.fmt.numChannels,
298
      this.fmt.sampleRate,
299
      '8a',
300
      alawmulaw.alaw.encode(output),
301
      {container: this.correctContainer_()});
302
  }
303
304
  /**
305
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
306
   * @param {string} bitDepthCode The new bit depth of the samples.
307
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
308
   *    Optional. Default is 16.
309
   */
310
  fromALaw(bitDepthCode='16') {
311
    this.fromScratch(
312
      this.fmt.numChannels,
313
      this.fmt.sampleRate,
314
      '16',
315
      alawmulaw.alaw.decode(this.data.samples),
316
      {container: this.correctContainer_()});
317
    if (bitDepthCode != '16') {
318
      this.toBitDepth(bitDepthCode);
319
    }
320
  }
321
322
  /**
323
   * Encode 16-bit wave file as 8-bit mu-Law.
324
   */
325
  toMuLaw() {
326
    this.assure16Bit_();
327
    let output = new Int16Array(this.data.samples.length / 2);
328
    unpackArrayTo(this.data.samples, this.dataType, output);
329
    this.fromScratch(
330
      this.fmt.numChannels,
331
      this.fmt.sampleRate,
332
      '8m',
333
      alawmulaw.mulaw.encode(output),
334
      {container: this.correctContainer_()});
335
  }
336
337
  /**
338
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
339
   * @param {string} bitDepthCode The new bit depth of the samples.
340
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
341
   *    Optional. Default is 16.
342
   */
343
  fromMuLaw(bitDepthCode='16') {
344
    this.fromScratch(
345
      this.fmt.numChannels,
346
      this.fmt.sampleRate,
347
      '16',
348
      alawmulaw.mulaw.decode(this.data.samples),
349
      {container: this.correctContainer_()});
350
    if (bitDepthCode != '16') {
351
      this.toBitDepth(bitDepthCode);
352
    }
353
  }
354
355
  /**
356
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
357
   * then it is created. It if exists, it is overwritten.
358
   * @param {string} tag The tag name.
359
   * @param {string} value The tag value.
360
   * @throws {Error} If the tag name is not valid.
361
   */
362
  setTag(tag, value) {
363
    tag = this.fixTagName_(tag);
364
    /** @type {!Object} */
365
    let index = this.getTagIndex_(tag);
366
    if (index.TAG !== null) {
367
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
368
        value.length + 1;
369
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
370
    } else if (index.LIST !== null) {
371
      this.LIST[index.LIST].subChunks.push({
372
        chunkId: tag,
373
        chunkSize: value.length + 1,
374
        value: value});
375
    } else {
376
      this.LIST.push({
377
        chunkId: 'LIST',
378
        chunkSize: 8 + value.length + 1,
379
        format: 'INFO',
380
        subChunks: []});
381
      this.LIST[this.LIST.length - 1].subChunks.push({
382
        chunkId: tag,
383
        chunkSize: value.length + 1,
384
        value: value});
385
    }
386
  }
387
388
  /**
389
   * Return the value of a RIFF tag in the INFO chunk.
390
   * @param {string} tag The tag name.
391
   * @return {?string} The value if the tag is found, null otherwise.
392
   */
393
  getTag(tag) {
394
    /** @type {!Object} */
395
    let index = this.getTagIndex_(tag);
396
    if (index.TAG !== null) {
397
      return this.LIST[index.LIST].subChunks[index.TAG].value;
398
    }
399
    return null;
400
  }
401
402
  /**
403
   * Return a Object<tag, value> with the RIFF tags in the file.
404
   * @return {!Object<string, string>} The file tags.
405
   */
406
  listTags() {
407
    /** @type {?number} */
408
    let index = this.getLISTINFOIndex_();
409
    /** @type {!Object} */
410
    let tags = {};
411
    if (index !== null) {
412
      for (let i=0; i<this.LIST[index].subChunks.length; i++) {
413
        tags[this.LIST[index].subChunks[i].chunkId] =
414
          this.LIST[index].subChunks[i].value;
415
      }
416
    }
417
    return tags;
418
  }
419
420
  /**
421
   * Remove a RIFF tag in the INFO chunk.
422
   * @param {string} tag The tag name.
423
   * @return {boolean} True if a tag was deleted.
424
   */
425
  deleteTag(tag) {
426
    /** @type {!Object} */
427
    let index = this.getTagIndex_(tag);
428
    if (index.TAG !== null) {
429
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
430
      return true;
431
    }
432
    return false;
433
  }
434
435
  /**
436
   * Create a cue point in the wave file.
437
   * @param {number} position The cue point position in milliseconds.
438
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
439
   */
440
  setCuePoint(position, labl='') {
441
    this.cue.chunkId = 'cue ';
442
    position = (position * this.fmt.sampleRate) / 1000;
443
    /** @type {!Array<!Object>} */
444
    let existingPoints = this.getCuePoints_();
445
    this.clearLISTadtl_();
446
    /** @type {number} */
447
    let len = this.cue.points.length;
448
    this.cue.points = [];
449
    /** @type {boolean} */
450
    let hasSet = false;
451
    if (len === 0) {
452
      this.setCuePoint_(position, 1, labl);
453
    } else {
454
      for (let i=0; i<len; i++) {
455
        if (existingPoints[i].dwPosition > position && !hasSet) {
456
          this.setCuePoint_(position, i + 1, labl);
457
          this.setCuePoint_(
458
            existingPoints[i].dwPosition,
459
            i + 2,
460
            existingPoints[i].label);
461
          hasSet = true;
462
        } else {
463
          this.setCuePoint_(
464
            existingPoints[i].dwPosition,
465
            i + 1,
466
            existingPoints[i].label);
467
        }
468
      }
469
      if (!hasSet) {
470
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
471
      }
472
    }
473
    this.cue.dwCuePoints = this.cue.points.length;
474
  }
475
476
  /**
477
   * Remove a cue point from a wave file.
478
   * @param {number} index the index of the point. First is 1,
479
   *    second is 2, and so on.
480
   */
481
  deleteCuePoint(index) {
482
    this.cue.chunkId = 'cue ';
483
    /** @type {!Array<!Object>} */
484
    let existingPoints = this.getCuePoints_();
485
    this.clearLISTadtl_();
486
    /** @type {number} */
487
    let len = this.cue.points.length;
488
    this.cue.points = [];
489
    for (let i=0; i<len; i++) {
490
      if (i + 1 !== index) {
491
        this.setCuePoint_(
492
          existingPoints[i].dwPosition,
493
          i + 1,
494
          existingPoints[i].label);
495
      }
496
    }
497
    this.cue.dwCuePoints = this.cue.points.length;
498
    if (this.cue.dwCuePoints) {
499
      this.cue.chunkId = 'cue ';
500
    } else {
501
      this.cue.chunkId = '';
502
      this.clearLISTadtl_();
503
    }
504
  }
505
506
  /**
507
   * Return an array with all cue points in the file, in the order they appear
508
   * in the file.
509
   * The difference between this method and using the list in WaveFile.cue
510
   * is that the return value of this method includes the position in
511
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
512
   * @return {!Array<!Object>}
513
   */
514
  listCuePoints() {
515
    /** @type {!Array<!Object>} */
516
    let points = this.getCuePoints_();
517
    for (let i=0; i<points.length; i++) {
518
      points[i].milliseconds =
519
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
520
    }
521
    return points;
522
  }
523
524
  /**
525
   * Update the label of a cue point.
526
   * @param {number} pointIndex The ID of the cue point.
527
   * @param {string} label The new text for the label.
528
   */
529
  updateLabel(pointIndex, label) {
530
    /** @type {?number} */
531
    let adtlIndex = this.getAdtlChunk_();
532
    if (adtlIndex !== null) {
533
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
534
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
535
            pointIndex) {
536
          this.LIST[adtlIndex].subChunks[i].value = label;
537
        }
538
      }
539
    }
540
  }
541
542
  /**
543
   * Set the string code of the bit depth based on the 'fmt ' chunk.
544
   * @private
545
   */
546
  bitDepthFromFmt_() {
547
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
548
      this.bitDepth = '32f';
549
    } else if (this.fmt.audioFormat === 6) {
550
      this.bitDepth = '8a';
551
    } else if (this.fmt.audioFormat === 7) {
552
      this.bitDepth = '8m';
553
    } else {
554
      this.bitDepth = this.fmt.bitsPerSample.toString();
555
    }
556
  }
557
  
558
  /**
559
   * Push a new cue point in this.cue.points.
560
   * @param {number} position The position in milliseconds.
561
   * @param {number} dwName the dwName of the cue point
562
   * @private
563
   */
564
  setCuePoint_(position, dwName, label) {
565
    this.cue.points.push({
566
      dwName: dwName,
567
      dwPosition: position,
568
      fccChunk: 'data',
569
      dwChunkStart: 0,
570
      dwBlockStart: 0,
571
      dwSampleOffset: position,
572
    });
573
    this.setLabl_(dwName, label);
574
  }
575
576
  /**
577
   * Return an array with all cue points in the file, in the order they appear
578
   * in the file.
579
   * @return {!Array<!Object>}
580
   * @private
581
   */
582
  getCuePoints_() {
583
    /** @type {!Array<!Object>} */
584
    let points = [];
585
    for (let i=0; i<this.cue.points.length; i++) {
586
      points.push({
587
        dwPosition: this.cue.points[i].dwPosition,
588
        label: this.getLabelForCuePoint_(
589
          this.cue.points[i].dwName)});
590
    }
591
    return points;
592
  }
593
594
  /**
595
   * Return the label of a cue point.
596
   * @param {number} pointDwName The ID of the cue point.
597
   * @return {string}
598
   * @private
599
   */
600
  getLabelForCuePoint_(pointDwName) {
601
    /** @type {?number} */
602
    let adtlIndex = this.getAdtlChunk_();
603
    if (adtlIndex !== null) {
604
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
605
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
606
            pointDwName) {
607
          return this.LIST[adtlIndex].subChunks[i].value;
608
        }
609
      }
610
    }
611
    return '';
612
  }
613
614
  /**
615
   * Clear any LIST chunk labeled as 'adtl'.
616
   * @private
617
   */
618
  clearLISTadtl_() {
619
    for (let i=0; i<this.LIST.length; i++) {
620
      if (this.LIST[i].format == 'adtl') {
621
        this.LIST.splice(i);
622
      }
623
    }
624
  }
625
626
  /**
627
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
628
   * @param {number} dwName The ID of the cue point.
629
   * @param {string} label The label for the cue point.
630
   * @private
631
   */
632
  setLabl_(dwName, label) {
633
    /** @type {?number} */
634
    let adtlIndex = this.getAdtlChunk_();
635
    if (adtlIndex === null) {
636
      this.LIST.push({
637
        chunkId: 'LIST',
638
        chunkSize: 4,
639
        format: 'adtl',
640
        subChunks: []});
641
      adtlIndex = this.LIST.length - 1;
642
    }
643
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
644
  }
645
646
  /**
647
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
648
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
649
   * @param {number} dwName The ID of the cue point.
650
   * @param {string} label The label for the cue point.
651
   * @private
652
   */
653
  setLabelText_(adtlIndex, dwName, label) {
654
    this.LIST[adtlIndex].subChunks.push({
655
      chunkId: 'labl',
656
      chunkSize: label.length,
657
      dwName: dwName,
658
      value: label
659
    });
660
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
661
  }
662
663
  /**
664
   * Return the index of the 'adtl' LIST in this.LIST.
665
   * @return {?number}
666
   * @private
667
   */
668
  getAdtlChunk_() {
669
    for (let i=0; i<this.LIST.length; i++) {
670
      if (this.LIST[i].format == 'adtl') {
671
        return i;
672
      }
673
    }
674
    return null;
675
  }
676
677
  /**
678
   * Return the index of the INFO chunk in the LIST chunk.
679
   * @return {?number} the index of the INFO chunk.
680
   * @private
681
   */
682
  getLISTINFOIndex_() {
683
    /** @type {?number} */
684
    let index = null;
685
    for (let i=0; i<this.LIST.length; i++) {
686
      if (this.LIST[i].format === 'INFO') {
687
        index = i;
688
        break;
689
      }
690
    }
691
    return index;
692
  }
693
694
  /**
695
   * Return the index of a tag in a FILE chunk.
696
   * @param {string} tag The tag name.
697
   * @return {!Object<string, ?number>}
698
   *    Object.LIST is the INFO index in LIST
699
   *    Object.TAG is the tag index in the INFO
700
   * @private
701
   */
702
  getTagIndex_(tag) {
703
    /** @type {!Object<string, ?number>} */
704
    let index = {LIST: null, TAG: null};
705
    for (let i=0; i<this.LIST.length; i++) {
706
      if (this.LIST[i].format == 'INFO') {
707
        index.LIST = i;
708
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
709
          if (this.LIST[i].subChunks[j].chunkId == tag) {
710
            index.TAG = j;
711
            break;
712
          }
713
        }
714
        break;
715
      }
716
    }
717
    return index;
718
  }
719
720
  /**
721
   * Fix a RIFF tag format if possible, throw an error otherwise.
722
   * @param {string} tag The tag name.
723
   * @return {string} The tag name in proper fourCC format.
724
   * @private
725
   */
726
  fixTagName_(tag) {
727
    if (tag.constructor !== String) {
728
      throw new Error('Invalid tag name.');
729
    } else if (tag.length < 4) {
730
      for (let i=0; i<4-tag.length; i++) {
731
        tag += ' ';
732
      }
733
    }
734
    return tag;
735
  }
736
737
  /**
738
   * Reset attributes that should emptied when a file is
739
   * created with the fromScratch() or fromBuffer() methods.
740
   * @private
741
   */
742
  clearHeader_() {
743
    this.fmt.cbSize = 0;
744
    this.fmt.validBitsPerSample = 0;
745
    this.fact.chunkId = '';
746
    this.ds64.chunkId = '';
747
  }
748
749
  /**
750
   * Make the file 16-bit if it is not.
751
   * @private
752
   */
753
  assure16Bit_() {
754
    this.assureUncompressed_();
755
    if (this.bitDepth != '16') {
756
      this.toBitDepth('16');
757
    }
758
  }
759
760
  /**
761
   * Uncompress the samples in case of a compressed file.
762
   * @private
763
   */
764
  assureUncompressed_() {
765
    if (this.bitDepth == '8a') {
766
      this.fromALaw();
767
    } else if (this.bitDepth == '8m') {
768
      this.fromMuLaw();
769
    } else if (this.bitDepth == '4') {
770
      this.fromIMAADPCM();
771
    }
772
  }
773
774
  /**
775
   * Set up the WaveFile object from a byte buffer.
776
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
777
   * @private
778
   */
779
  interleave_(samples) {
780
    if (samples.length > 0) {
781
      if (samples[0].constructor === Array) {
782
        /** @type {!Array<number>} */
783
        let finalSamples = [];
784
        for (let i=0; i < samples[0].length; i++) {
785
          for (let j=0; j < samples.length; j++) {
786
            finalSamples.push(samples[j][i]);
787
          }
788
        }
789
        samples = finalSamples;
790
      }
791
    }
792
    return samples;
793
  }
794
795
  /**
796
   * Update the type definition used to read and write the samples.
797
   * @private
798
   */
799
  updateDataType_() {
800
    /** @type {!Object} */
801
    this.dataType = {
802
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
803
      float: this.bitDepth == '32f' || this.bitDepth == '64',
804
      signed: this.bitDepth != '8',
805
      be: this.container == 'RIFX'
806
    };
807
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
808
      this.dataType.bits = 8;
809
      this.dataType.signed = false;
810
    }
811
  }
812
813
  /**
814
   * Return 'RIFF' if the container is 'RF64', the current container name
815
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
816
   * @return {string}
817
   * @private
818
   */
819
  correctContainer_() {
820
    return this.container == 'RF64' ? 'RIFF' : this.container;
821
  }
822
}
823